X Tracking Pixel - #59
Conversation
Greptile SummaryThe PR adds the X Ads universal website tag globally and now reports pageviews after App Router navigations.
Confidence Score: 2/5The PR is not safe to merge until the X pixel is permitted by the production CSP and tracking is gated by a consent preference. The navigation-pageview fix is present, but Cloudflare Pages still blocks the third-party script and the globally mounted pixel still initializes and reports route changes without consulting a consent or withdrawal state. Files Needing Attention: src/components/XPixel.tsx, src/app/layout.tsx, public/_headers
|
| Filename | Overview |
|---|---|
| src/app/layout.tsx | Globally mounts the X pixel, but the previously reported consent boundary remains unenforced. |
| src/components/XPixel.tsx | Adds effective client-navigation reporting, while the production CSP and consent issues remain unresolved. |
Reviews (2): Last reviewed commit: "Report X pixel page views on client navi..." | Re-trigger Greptile
| <SubscribeModal /> | ||
| <IdentifyUser /> | ||
| <HubspotTracking /> | ||
| <XPixel /> |
There was a problem hiding this comment.
Advertising pixel bypasses consent
When any visitor opens a production route, the globally mounted pixel reports the initial page view without consulting a consent preference, so visitors cannot withhold or withdraw the consent promised by the privacy notice. How this was verified: The root layout mounts XPixel unconditionally, its only guard checks NODE_ENV, and the repository contains no consent or opt-out mechanism.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/layout.tsx
Line: 81
Comment:
**Advertising pixel bypasses consent**
When any visitor opens a production route, the globally mounted pixel reports the initial page view without consulting a consent preference, so visitors cannot withhold or withdraw the consent promised by the privacy notice. **How this was verified:** The root layout mounts `XPixel` unconditionally, its only guard checks `NODE_ENV`, and the repository contains no consent or opt-out mechanism.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Real gap, but pre-existing and out of scope for this PR.
You are right that the privacy notice says "We rely on your consent, which you can withdraw" (src/app/privacy-notice/page.tsx:32) and there is no consent mechanism in the repo. But that is not something this PR introduces: GA, HubSpot, and PostHog all already load unconditionally on every production page view, gated only on NODE_ENV. The X pixel follows the exact same existing pattern.
Gating only the X pixel would be incoherent — it would leave three other trackers firing pre-consent while implying the problem was solved. A consent banner plus a preference store that gates all four belongs in its own PR. Flagged to the team rather than fixed here.
uwt.js installs no history listener, so App Router route changes went
unreported. Mirror the HubSpot tracker: watch pathname/searchParams and
re-call twq('config', ...), which fires a fresh page-load beacon on every
invocation (no init-once guard in the tag).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the X (Twitter) Ads universal website tag so we can attribute conversions from X ad campaigns.
What's here
src/components/XPixel.tsx— the tag, pixel IDre2t6, loadedafterInteractive.src/app/layout.tsx— mounted once in the root layout.Production only (
NODE_ENVguard), so localhost and preview deployments stay out of the ad data.Client-side navigation
uwt.jsinstalls no history listener — the source has no references topushState,replaceState,popstate, orhashchange— so the tag reports only the initial page load and every App Router navigation would go unreported.PageViewTrackerhandles this the same wayHubspotTrackingalready does: watchusePathname()/useSearchParams()and report each route change, skipping the first render since the inline snippet already covered it.Route changes re-call
twq('config', ...)rather than the legacytwq('track', 'PageView'). Both APIs live inuwt.js— the legacy UWT (init/track) and the current X Pixel (config/event/set) — and this pixel uses the latter. Re-callingconfigis safe and correct: its implementation unconditionally invokesl.track({… eci: ONETAG_CONFIG …})with no init-once guard, so every invocation emits a fresh page-load beacon.Notes for review
No CSP change needed.
public/_headershas ascript-srcpolicy that omitsstatic.ads-twitter.com, but that file is dead config here — it's a Cloudflare Pages/Netlify convention and this app deploys via Docker, which never reads it.next.config.tsalready documents this, settingContent-Signalas a real Next.js header "rather than public/_headers because the Docker deploy doesn't read that file." The same allowlist also omits googletagmanager, hs-scripts, and PostHog, all of which work in production today. Deleting that stale file or porting its CSP intonext.config.tsis worth a separate PR; the ads-twitter origin should be included if that happens.Consent gating is out of scope. The privacy notice says we rely on consent that can be withdrawn, and there's no consent mechanism in the repo — but this PR doesn't introduce that gap. GA, HubSpot, and PostHog all already load unconditionally on every production page view, gated only on
NODE_ENV, and this pixel follows the identical pattern. Gating only the X pixel would leave three trackers firing pre-consent while implying it was solved. A consent banner and preference store covering all four belongs in its own PR.Testing
tsc --noEmitandeslintare clean, and the app compiles.Not verified end-to-end: the
NODE_ENVguard makes the pixel a no-op locally, so the route-change beacon is verified by reading the tag source, not by observing a live request. Worth a spot-check in the X Ads event manager after deploy.Unrelated:
next buildcurrently fails prerendering/trackerbecause the elections API 404s locally. Confirmed pre-existing — the same failure reproduces onmainwith this branch's changes stashed.